<template>
  <div ref="wrapperRef" class="elem-plus-mention-container" :style="containerStyle">
    <div ref="elInputRef" class="textarea" contenteditable="true" @compositionstart="handleCompositionStart"
      :aria-activedescendant="dropdownVisible ? hoveringId || '' : undefined"
      :aria-controls="dropdownVisible ? contentId : undefined" :aria-expanded="dropdownVisible || undefined"
      :aria-label="ariaLabel" :aria-autocomplete="dropdownVisible ? 'none' : undefined"
      :aria-haspopup="dropdownVisible ? 'listbox' : undefined" :style="inputStyle" @input="handleInputChange"
      @mousedown="handleInputMouseDown" @compositionend="handleCompositionEnd" @keydown="handleInputKeyDown"
      tabindex="0" v-html="contentLabel"></div>

    <el-tooltip ref="tooltipRef" :visible="dropdownVisible" :popper-class="[ns.e('popper'), popperClass]"
      :popper-options="popperOptions" :placement="computedPlacement" :fallback-placements="computedFallbackPlacements"
      effect="light" pure :offset="offset" :show-arrow="showArrow">
      <template #default>
        <div :style="cursorStyle" />
      </template>
      <template #content>
        <MentionDropdown ref="dropdownRef" :options="options" :disabled="disabled" :loading="loading"
          :content-id="contentId" :aria-label="ariaLabel" @select="handleSelect" @click.stop="elInputRef?.focus">
          <template v-for="(_, name) in $slots" #[name]="slotProps">
            <slot :name="name" v-bind="slotProps" />
          </template>
        </MentionDropdown>
      </template>
    </el-tooltip>

  </div>
</template>

<script setup lang="ts" name="Mention">
import 'element-plus/dist/index.css'
import { computed, nextTick, ref, shallowRef } from 'vue'
import type { Ref, CSSProperties } from 'vue'
import type { TooltipInstance } from 'element-plus'
import { ElTooltip } from 'element-plus'
import MentionDropdown from './MentionDropdown.vue'
import { useNamespace, useFocusController } from 'element-plus'
import { mentionEmits, mentionProps } from './Mention'
import type { MentionCtx, MentionOption } from './types'
import { EVENT_CODE } from './types'
import { UPDATE_MODEL_EVENT, INPUT_EVENT, SELECT_EVENT, SEARCH_EVENT, INPUT_VALUE_EVENT } from './event'
import { isFunction } from "@vue/shared";
import { getCursorPosition, getMentionCtx, getPositionInParent } from './helper'

type TargetElement = HTMLDivElement | HTMLInputElement | HTMLTextAreaElement

const props = defineProps(mentionProps)
const emit = defineEmits(mentionEmits)

const dealContentVal2Html = (val: string) => {
  const variableLebel = (variable: string) => {
    return props.options.find(item => item.value === variable)?.label || variable;
  }
  const variableIcon = (variable: string) => {
    return props.options.find(item => item.value === variable)?.icon || null;
  }
  const template = (variableList: string[]) => {
    const icon = variableIcon(variableList[0]);
    const str = '<span class="tag" contenteditable="false" val="' + variableList.join('.') + '">'
      + (icon ? '<img class="icon" src="' + icon + '" style="object-fit: contain;">' : '')
      + `<i class="tag-text">${variableLebel(variableList[0])}</i>`
      + (variableList.length > 1 ? '<i class="tag-divider">❯</i>' : '')
      + (variableList.length > 1 ? `<i class="tag-text">${variableLebel(variableList[1])}</i>` : '')
      + '</span>';
    return str;
  };

  const content = val.replace(/\{\{\$([\.\w]+)\$\}\}/g, (_, variable) => {
    return template(variable.split('.'))
  })

  return content
}
const dealContentVal2Value = (val: string) => {
  const regex = /<span class="tag" contenteditable="false" val="([^"]+)">([\s\S]*?)<\/span>/g;
  return val.replace(regex, (_, variable) => {
    return `{{\$${variable}\$}}`
  })
}
const clearHiddenContentVal2Value = (val: string) => {
  const regex = new RegExp(`<span\\s+class=\"custom-marker\"><\/span>`, 'g');
  return val.replace(regex, '')
}

const contentValue = ref(props?.modelValue || "");
const contentLabel = ref(dealContentVal2Html(contentValue.value));

const ns = useNamespace('mention')
const disabled: Ref<boolean> = ref(false)
const contentId = ref('mz-element-plus-vue-mention')

const isComposing = ref(false)
const elInputRef = shallowRef<TargetElement>()
const tooltipRef = ref<TooltipInstance>()
const dropdownRef = ref<InstanceType<typeof MentionDropdown>>()

const visible = ref(false)
const cursorStyle = ref<CSSProperties>()
const mentionCtx = ref<MentionCtx>()

const computedPlacement = computed<any>(() =>
  props.showArrow ? props.placement : `${props.placement}-start`
)

const computedFallbackPlacements = computed<any[]>(() =>
  props.showArrow ? ['bottom', 'top'] : ['bottom-start', 'top-start']
)

const filteredOptions = computed(() => {
  const { filterOption, options } = props
  if (!mentionCtx.value || !filterOption) return options
  return options.filter((option) =>
    filterOption(mentionCtx.value!.pattern, option)
  )
})

const dropdownVisible = computed(() => {
  return visible.value && (!!filteredOptions.value.length || props.loading)
})

const hoveringId = computed(() => {
  return `${contentId.value}-${dropdownRef.value?.hoveringIndex}`
})

const handleInputChange = (event: InputEvent | any) => {
  const html: string = (event.currentTarget as HTMLElement).innerHTML || "",
    value = event?.data || "";

  contentValue.value = dealContentVal2Value(html)
  contentValue.value = clearHiddenContentVal2Value(contentValue.value)

  emit(UPDATE_MODEL_EVENT, contentValue.value)
  emit(INPUT_EVENT, contentValue.value)
  emit(INPUT_VALUE_EVENT, value)

  syncAfterCursorMove()
}

const handleCompositionStart = () => {
  isComposing.value = true
}

const handleCompositionEnd = () => {
  isComposing.value = false
}

const handleInputKeyDown = (event: KeyboardEvent | Event) => {
  if (!('code' in event) || isComposing.value) return

  switch (event.code) {
    case EVENT_CODE.left:
    case EVENT_CODE.right:
      syncAfterCursorMove()
      break
    case EVENT_CODE.up:
    case EVENT_CODE.down:
      if (!visible.value) return
      event.preventDefault()
      dropdownRef.value?.navigateOptions(
        event.code === EVENT_CODE.up ? 'prev' : 'next'
      )
      break
    case EVENT_CODE.enter:
    case EVENT_CODE.numpadEnter:
      if (!visible.value) return
      event.preventDefault()
      if (dropdownRef.value?.hoverOption) {
        dropdownRef.value?.selectHoverOption()
      } else {
        visible.value = false
      }
      break
    case EVENT_CODE.esc:
      if (!visible.value) return
      event.preventDefault()
      visible.value = false
      break
    case EVENT_CODE.backspace:
      if (props.whole && mentionCtx.value) {
        const { splitIndex, selectionEnd, pattern, prefixIndex, prefix } =
          mentionCtx.value
        const inputEl = getInputEl()
        if (!inputEl) return
        const inputValue = contentValue.value
        const matchOption = props.options.find((item) => item.value === pattern)
        const isWhole = isFunction(props.checkIsWhole)
          ? props.checkIsWhole(pattern, prefix)
          : matchOption
        if (isWhole && splitIndex !== -1 && splitIndex + 1 === selectionEnd) {
          event.preventDefault()
          const newValue =
            inputValue.slice(0, prefixIndex) + inputValue.slice(splitIndex + 1)
          emit(UPDATE_MODEL_EVENT, newValue)
          emit(INPUT_EVENT, newValue)

          nextTick(() => {
            // input value is updated
            // TODO 
            // inputEl.selectionStart = newSelectionEnd
            // inputEl.selectionEnd = newSelectionEnd
            syncDropdownVisible()
          })
        }
      }
  }
}

const { wrapperRef } = useFocusController(elInputRef, {
  beforeFocus() {
    return disabled.value
  },
  afterFocus() {
    syncAfterCursorMove()
  },
  beforeBlur(event) {
    return tooltipRef.value?.isFocusInsideContent(event)
  },
  afterBlur() {
    visible.value = false
  },
})

const handleInputMouseDown = () => {
  syncAfterCursorMove()
}

const handleSelect = (item: MentionOption) => {
  if (!mentionCtx.value) return
  const inputEl = getInputEl()
  if (!inputEl) return

  const inputValue = dealContentVal2Value(inputEl.innerHTML)
  const { split, prefix } = props

  const newEndPart = inputValue.slice(mentionCtx.value.end)
  const alreadySeparated = newEndPart.startsWith(split)
  const parentContext = item.parent ? item.parent + '.' : '';
  const newMiddlePart = `{{\$${parentContext}${item.value}\$}}${alreadySeparated ? '' : split}`

  const prefixStr = typeof prefix === 'string' ? prefix : prefix.join('|')
  const regex = new RegExp(`[${prefixStr}]<span\\s+class=\"custom-marker\"><\/span>`, 'g');
  const newValue = inputValue.replace(regex, newMiddlePart);

  contentLabel.value = dealContentVal2Html(newValue);

  emit(UPDATE_MODEL_EVENT, newValue)
  emit(INPUT_EVENT, newValue)
  emit(SELECT_EVENT, item, mentionCtx.value.prefix)

  nextTick(() => {
    // input value is updated
    syncDropdownVisible()
    setTimeout(() => {
      // TODO 目前只能让input失去焦点，没有找到解决办法
      inputEl.blur()
    }, 200)
  })
}

const getInputEl = () => {
  return elInputRef.value
}

const syncAfterCursorMove = () => {
  // can't use nextTick(), get cursor position will be wrong
  setTimeout(() => {
    syncDropdownVisible()
    syncCursor()
    nextTick(() => tooltipRef.value?.updatePopper())
  }, 0)
}

const syncCursor = () => {
  const inputEl = getInputEl()
  if (!inputEl) return

  const caretPosition = getCursorPosition(inputEl)
  if (!wrapperRef?.value) {
    return
  }

  const list = wrapperRef.value.querySelectorAll('.custom-marker');
  if (wrapperRef.value && list.length) {
    const childRect = getPositionInParent(list[0]);
   
    const left = childRect.x / props.zoom;
    const top = childRect.y / props.zoom;

    // 计算相对坐标（考虑滚动）
    cursorStyle.value = {
      position: 'absolute',
      width: 0,
      height: `${caretPosition.height}px`,
      left: left + `px`,
      top: top + `px`,
    }
  }

}

const syncDropdownVisible = () => {
  const inputEl = getInputEl();
  if (document.activeElement !== inputEl) {
    visible.value = false
    return
  }
  const { prefix, split } = props
  mentionCtx.value = getMentionCtx(inputEl, prefix, split)
  console.log("mentionCtx:", mentionCtx.value)
  if (mentionCtx.value && mentionCtx.value.splitIndex === -1) {
    visible.value = true
    emit(SEARCH_EVENT, mentionCtx.value.pattern, mentionCtx.value.prefix)
    return
  }
  visible.value = false
}

defineExpose({
  input: elInputRef,
  tooltip: tooltipRef,
  dropdownVisible,
})

</script>

<style lang="scss" scoped>
* {
  outline: none;
}

.elem-plus-mention-container {
  text-align: left;
  user-select: text;
  white-space: pre-wrap;
  word-break: break-word;
  position: relative;

  :deep(.tag) {
    background-color: #F0F4FF;
    color: #485264;
    border-radius: 4px;
    margin: 0 6px;
    box-sizing: border-box;
    white-space: pre-wrap;
    word-break: break-word;
    display: inline-block;
    padding: 0 4px;
    line-height: 20px;

    .icon {
      width: 16px;
      vertical-align: -2px;
      border-radius: 4px;
      margin-right: 4px;
    }

    .tag-divider {
      margin: 0 4px;
      font-size: 12px;
    }
  }

  .textarea {
    height: 200px;
    width: 500px;
    padding: 4px;
    overflow-y: auto;
    border-radius: 4px;
    border: 1px solid rgb(232, 235, 240);

    .custom-marker {
      visibility: hidden;
      width: 0;
    }
  }

  .textarea:hover {
    border: 1px solid rgb(51, 112, 255) !important;
  }

  .textarea:focus {
    border: 1px solid rgb(51, 112, 255) !important;
  }

}
</style>